package com.ryenlii.datatrans.core.config;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.date.StopWatch;
import com.eyelake.common.core.exception.BizException;
import com.eyelake.iot.common.tsdb.base.properties.TsdbProperties;
import com.eyelake.iot.common.tsdb.base.utlis.StrTransformUtil;
import com.taosdata.jdbc.TSDBDriver;
import com.taosdata.jdbc.TSDBPreparedStatement;
import jakarta.annotation.Resource;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;

import java.lang.reflect.Method;
import java.sql.*;
import java.time.Instant;
import java.util.Date;
import java.util.*;

;

/**
 * @Author wws
 * @Date 2023/5/10 10:59
 */
@Configuration
@ConditionalOnProperty(prefix = "iot", name = "tsdbType", havingValue = "tdenginedb")
@EnableConfigurationProperties({TsdbProperties.class})
@Slf4j
public class TdengineDbConfiguration {

    private Connection connection;
    @Resource
    private TsdbProperties tsdbProperties;

    @Getter
    @Value("${iot.switchF:false}")
    private Boolean switchF;

    /**
     * 批量处理记录数
     */
    private static final int batchSize = 100;

    /**
     * 功能描述:
     * 插入使用模板
     *
     * @param templateTable 模板表
     * @param targetTable   目标表
     * @param fields        字段
     * @param tags          标签
     * @author wws
     * @date 2023-05-18 09:20:45
     */
    public int insertUsingTemplate(String templateTable, String targetTable,
                                   Map<String, Object> fields,
                                   Map<String, String> tags, long uploadDataTime) {
        connect();
        StringBuilder sb = getTableQuery(templateTable, targetTable, fields, tags);
        try {
            PreparedStatement statement = connection.prepareStatement(sb.toString());
            // 下标1开始
            int index = 1;
            for (String value : tags.values()) {
                statement.setString(index++, value);
            }
            // 插入时间序列
            statement.setLong(index++, uploadDataTime);

            for (Object value : fields.values()) {
                statement.setObject(index++, value);
            }
            return statement.executeUpdate();
        } catch (SQLException e) {
            throw new RuntimeException(e);
        } finally {
            disconnect();
        }
    }

    private StringBuilder getTableQuery(String templateTable, String targetTable,
                                        Map<String, Object> fields, Map<String, String> tags) {
        StringBuilder sb = new StringBuilder();
        sb.append("INSERT INTO ");
        sb.append(targetTable);
        sb.append(" USING ");
        sb.append(templateTable);
        sb.append(" TAGS (");
        sb.append(getDynamicPlaceholder(tags.size()));
        sb.append(") VALUES (");
        // 保留时间序列行,filed首位插入时间
        sb.append(getDynamicPlaceholder(fields.size() + 1));
        sb.append(")");
        return sb;
    }

    private String getDynamicPlaceholder(int size) {
        return String.join(",", Collections.nCopies(size, "?"));
    }


    /**
     * 使用模板表批量插入数据到目标表。
     *
     * @param templateTable 模板表名，用于构造插入语句的表结构。
     * @param targetTable   目标表名，数据将被插入到此表。
     * @param fields        包含要插入的数据的列表，每个元素是一个Map，其中key是字段名，value是字段值。
     * @param tags          用于构造插入语句的标签，这些标签的值将被设置到插入语句中。
     */
    public void insertBatchUsingTemplate(String templateTable, String targetTable,
                                         List<Map<String, Object>> fields,
                                         Map<String, String> tags) {
        // 获取第一个数据项，用于分析字段类型等
        Map<String, Object> first = CollUtil.getFirst(fields);
        // 构造SQL查询语句
        StringBuilder sb = getTableQuery(templateTable, targetTable, first, tags);
        try {
            // 初始化计时器，用于测量插入数据的耗时
            StopWatch stopWatch = new StopWatch();
            stopWatch.start();

            // 准备执行的SQL语句
            TSDBPreparedStatement statement = (TSDBPreparedStatement) connection.prepareStatement(sb.toString());
            // 设置标签值到PreparedStatement中
            int index = 1;
            for (String value : tags.values()) {
                statement.setObject(index++, value);
            }
            // 关闭自动提交，以提高数据插入性能
            connection.setAutoCommit(false);
            // 设置时间戳，用于区分不同的数据插入时间
            long ts = DateUtil.current();

            for (int i = 0; i < fields.size(); i++) {
                int idx = index;
                Map<String, Object> fieldMap = fields.get(i);
                // 设置时间戳和字段值
                statement.setTimestamp(idx++, new Timestamp(ts + i));
                for (Object value : fieldMap.values()) {
                    statement.setObject(idx++, value);
                }
                // 将当前数据项加入到批处理中
                statement.columnDataAddBatch();
                // 每处理1000个数据项，执行一次批处理，清空批处理队列
                if (i != 0 && i % batchSize == 0) {
                    statement.columnDataExecuteBatch();
                    statement.clearBatch();
                } else if (i == fields.size() - 1) {
                    statement.executeBatch();
                }
            }

            // 提交批处理，完成数据插入
            connection.commit();
            stopWatch.stop();
            // 记录数据插入的耗时
            log.info("批量插入数据耗时:{}ms", stopWatch.getTotalTimeMillis());
            connection.setAutoCommit(true);
        } catch (SQLException e) {
            log.error("批量插入数据异常", e);
            // 处理数据库操作异常
            throw new RuntimeException(e);
        }
    }

    /**
     * 功能描述:
     * 连接
     *
     * @return
     * @throws ClassNotFoundException 类没有发现异常
     * @throws SQLException           sqlexception异常
     * @author wws
     * @date 2023-05-10 13:58:17
     */
    public Connection connect() {
        log.info("开启Tdengine连接>>>>>>>>>>>>");
        try {
            if (connection == null || connection.isClosed()) {
                // com.taosdata.jdbc.rs.RestfulDriver
                Class.forName("com.taosdata.jdbc.TSDBDriver");
                String jdbcUrl = tsdbProperties.getUrl() + "/" + tsdbProperties.getDatabase();
                Properties connProps = getProperties();
                this.connection = DriverManager.getConnection(jdbcUrl, connProps);
            }
        } catch (Exception e) {
            log.error("开启TD连接异常:{}", e.getMessage());
            throw BizException.getInstance("开启TD连接异常");
        }
        return this.connection;
    }

    private Properties getProperties() {
        Properties connProps = new Properties();
        connProps.setProperty(TSDBDriver.PROPERTY_KEY_USER, tsdbProperties.getUser());
        connProps.setProperty(TSDBDriver.PROPERTY_KEY_PASSWORD, tsdbProperties.getPassword());
        connProps.setProperty(TSDBDriver.PROPERTY_KEY_CONFIG_DIR, "/etc/taos");
        connProps.setProperty(TSDBDriver.PROPERTY_KEY_CHARSET, "UTF-8");
        connProps.setProperty(TSDBDriver.PROPERTY_KEY_LOCALE, "en_US.UTF-8");
        connProps.setProperty(TSDBDriver.PROPERTY_KEY_TIME_ZONE, "Shanghai");
        return connProps;
    }

    /**
     * 功能描述:
     * 断开连接
     *
     * @throws SQLException sqlexception异常
     * @author wws
     * @date 2023-05-10 13:57:14
     */
    public void disconnect() {
        log.info("关闭连接<<<<<<<<<<<<");
        try {
            if (connection != null && !connection.isClosed()) {
                connection.close();
            }
        } catch (SQLException e) {
            log.error("关闭连接异常:{}", e.getMessage());
            throw BizException.getInstance("关闭连接异常");
        }
    }

    /**
     * 功能描述:
     * 执行查询
     *
     * @param sql sql
     * @return {@code List<Map<String, Object>> }
     * @author wws
     * @date 2023-05-11 09:59:21
     */
    public List<Map<String, Object>> executeQuery(String sql) {
        connect();
        log.info("executeQuery执行sql:{}", sql);
        try (Statement stmt = connection.createStatement();
             ResultSet rs = stmt.executeQuery(sql)) {
            ResultSetMetaData rsmd = rs.getMetaData();
            int columnCount = rsmd.getColumnCount();
            List<Map<String, Object>> result = new ArrayList<>();

            while (rs.next()) {
                Map<String, Object> row = new HashMap<>();
                for (int i = 1; i <= columnCount; i++) {
                    String columnName = rsmd.getColumnLabel(i);
                    Object value = rs.getObject(i);
                    row.put(columnName, value);
                }
                result.add(row);
            }
            return result;
        } catch (SQLException e) {
            log.error("执行SQL异常:{}", e.getMessage());
            throw BizException.getInstance("执行SQL异常");
        } finally {
            disconnect();
        }
    }

    /**
     * 执行sql（无论是否返回结果），将结果注入到指定的类型实例中，且返回
     * 当查询到的结果没有时，返回一个大小为0的list;
     * <p>
     * 对象遵从以下说明<br/>
     * 1.对象字段为String类型，数据库类型(通过jdbc读取到的)无论什么类型，都将调用Object.toString方法注入值<br/>
     * 2.对象字段为数据库类型(通过jdbc读取到的)一致的情况下，将会直接注入<br/>
     * 3.对象字段与数据库类型(通过jdbc读取到的)不一致的情况下，将尝试使用{@link Class#cast(Object)}方法转型，失败此值会是类型默认值(故实体推荐使用封装类型)<br/>
     * 4.对象字段为{@link Date}时，数据库类型为Date才可以注入，如果为long(例如TDengine)将会被当作毫秒的时间戳注入<br/>
     *
     * @param sql   要执行的sql
     * @param clazz 要注入的实体类型
     * @param <T>   要注入的实体类型
     * @return
     * @throws IllegalAccessException
     * @throws InstantiationException
     * @throws SQLException
     */
    public <T> List<T> getList(String sql, Class<T> clazz) {
        connect();
        List<T> list = new ArrayList<>();
        log.info("getList执行sql:{}", sql);
        try {
            Method[] setterMethods = StrTransformUtil.getSetterMethods(clazz);
            ResultSet resultSet = connection.createStatement().executeQuery(sql);

            while (resultSet.next()) {
                list.add(resultSetToObject(resultSet, setterMethods, clazz));
            }
        } catch (Exception e) {
            log.error("执行Sql异常:{}", e.getMessage());
            throw BizException.getInstance("执行Sql异常");
        } finally {
            disconnect();
        }
        return list;
    }

    /**
     * 功能描述:
     * 数量查询
     *
     * @param sql sql
     * @return long
     * @author wws
     * @date 2023-05-22 17:36:59
     */
    public long executeCountQuery(String sql) {
        connect();
        try (Statement stmt = connection.createStatement();
             ResultSet rs = stmt.executeQuery(sql)) {
            if (rs.next()) {
                return rs.getLong(1);
            } else {
                return 0;
            }
        } catch (SQLException sqlException) {
            throw BizException.getInstance("暂无数据");
        } finally {
            disconnect();
        }
    }

    /**
     * 将resultSet注入到指定的类型实例中，且返回
     * 对象遵从以下说明<br/>
     * 1.对象字段为String类型，数据库类型(通过jdbc读取到的)无论什么类型，都将调用Object.toString方法注入值<br/>
     * 2.对象字段为数据库类型(通过jdbc读取到的)一致的情况下，将会直接注入<br/>
     * 3.对象字段与数据库类型(通过jdbc读取到的)不一致的情况下，将尝试使用{@link Class#cast(Object)}方法转型，失败此值会是类型默认值(故实体推荐使用封装类型)<br/>
     * 4.对象字段为{@link Date}时，数据库类型为Date才可以注入，如果为long(例如TDengine)将会被当作毫秒的时间戳注入<br/>
     * <p>
     * 注意，此方法只会注入一个结果,不会循环{@link ResultSet#next()}方法，请从外部调用。<br/>
     * 传入setterMethods的目的是为了方便外部循环使用此方法，这样方法内部不会重复调用，提高效率<br/>
     *
     * @param resultSet 查询结果，一定要是{@link ResultSet#next()}操作过的，不然没有数据
     * @param clazz     注入对象类型
     * @param <T>       注入对象类型
     * @return
     * @throws IllegalAccessException
     * @throws InstantiationException
     */
    public <T> T resultSetToObject(ResultSet resultSet, Method[] setterMethods, Class<T> clazz)
            throws IllegalAccessException {
        T result;
        try {
            result = clazz.newInstance();
        } catch (InstantiationException e) {
            log.error("请检查类{}是否有无参构造方法", clazz.getCanonicalName());
            log.error("参构造方法:{}", e.getMessage());
            throw BizException.getInstance("参构造方法异常");
        }

        for (Method method : setterMethods) {
            try {
                if (method == null) {
                    continue;
                }
                String fieldName = StrTransformUtil.getFieldNameBySetter(method);
                // 因为标准的setter方法只会有一个参数，所以取一个就行了
                Class<?> getParamClass = method.getParameterTypes()[0];

                // 获得查询的结果
                Object resultObject;

                try {
                    resultObject = resultSet.getObject(fieldName);
                } catch (SQLException e) {
                    continue;
                }
                if (resultObject == null) {
                    continue;
                }
                // 如果实体类的类型是String类型，那么无论x数据库类型是什么，都调用其toString方法获取值
                if (getParamClass.equals(String.class)) {
                    method.invoke(result, resultSet.getString(fieldName));
                } else if (getParamClass.equals(Instant.class)) {
                    Timestamp timestamp = resultSet.getTimestamp(fieldName);
                    method.invoke(result, new Date(timestamp.getTime()).toInstant());
                } else if (getParamClass.equals(Double.class)) {
                    method.invoke(result, resultSet.getDouble(fieldName));
                } else {

                    try {
                        method.invoke(result, resultObject);
                    } catch (IllegalArgumentException e) {
                        // 对象字段与数据库类型(通过jdbc读取到的)不一致的情况下，将尝试强制转型
                        method.invoke(result, getParamClass.cast(resultObject));
                    }
                }
            } catch (Exception e) {
                log.error("查询异常:{}", e.getMessage());
            }
        }
        return result;
    }

}
